/**************************************************************************************** * <b>LibHTTP</b>, small yet powerful library to simplify the use of HTTP in scripts. * <p> * Takes care of proxies, authentication, URL parsing, making the actual request, parsing * headers, and following redirects (HTTP-redirects only). After this "boring" work is * complete, LibHTTP transfers control back to the calling script, giving you full freedom * to do whatever your need to do with the data. All headers will have been consumed at * this point - the data you get from the socket with sockread is the document, without * headers. LibHTTP accomplishes this by opening the socket with a different [temporary] * name, and renaming it back to what you specified after it has finished its task. * <p> * Right click status window and select "Config HTTP Proxy" to do just that. The dialog * should be clear enough for end users to understand but if you already have a config * dialog in your script you may want to move the options there instead. Proxy support is * automatic and transparent: making and handling the requests works exactly the same way * whether or not a proxy is used - your script doesn't need to do anything special for * proxies to work. * <p> * <code>/http_get SOCKETNAME %URL<br> * on *:sockread:SOCKETNAME {<br> * [read data and do things with it]<br> * }</code> * <p> * <hr width="250" align="left"> * Errors are communicated with signal (replace "SocketName" in the signal name):<br> * <code>on *:signal:HttpErr_SocketName: /echo -s HTTP connection error: $1-</code><br> * <p> * This signal is triggered if * <ul> * <li>LibHTTP is unable to connect to the server * <li>the connection is lost while reading headers * <li>server response code != 200 (200 means all went ok) * <li>redirection limit is exceeded (limit is 5 redirections) * </ul> * The socket is automatically closed if any of these errors occur, meaning your * custom sockread and sockclose event handlers are never executed. The reverse of this * also guaranteed - if your sockread handler is executed you can 100% sure everything * went ok. You don't <i>have to</i> catch and handle the error signal, but in most * cases you <i>should</i> do it, at least to echo the message to status window. * Without any kind of error message users will have no idea what is going wrong and * how to fix it. * <p> * Of course you still need to test for $sockerr in the sockread and sockclose events, * just like you have to with any other socket. LibHTTP detects errors only while * connecting and reading headers - after this all control, including responsibilty * to detect and handle errors, is yours. * <hr width="250" align="left"> * <p> * <b>Note:</b> You can not use <code>/sockmark</code> with connections opened * by LibHTTP, the library uses the mark for storing various data. * <p> * <code>/set %libhttp.debug 1</code> - enables some debugging messages. Unset to disable. * */ /**************************************************************************************** * Test whether Sockname is already in use. The normal test '<code>if ($sock(SOCKNAME) != * $null)</code>' is not enough because <code>/http_get</code> opens the sockets using * temporary names (these are later renamed to the final form). $sockfree will also check * the temporary names to see if there is a socket that will soon be renamed to Sockname. * @param sockname The socket name to test * @return boolean $true if Socketname is free, $false if not */ alias sockfree { var %sockname = $1 if ($sock(%sockname)) return $false var %i = 1 var %tempsock = $sock(LIBHTTP_TEMP_*,%i) while (%tempsock != $null) { if ($hget(%tempsock,_Final-Sockname) == %sockname) return $false inc %i var %tempsock = $sock(LIBHTTP_TEMP_*,%i) } return $true } /**************************************************************************************** * Request one file from remote server. * @param Sockname Name for this connections * @param URL Location of the file * @return void */ alias http_get { var %sockname = $1, %url = $2 if (!$sockfree(%sockname)) { echo -sat HTTP socket error: socket name %sockname already in use halt } lhttp_sockopen %sockname 0 $lhttp_parse_url(%url) } /**************************************************************************************** * Retrieve special information about Sockname. Uses same syntax as mIRC's * <code>$sock(...).property</code>, meaning you can specify wildcard Sockname and select * Nth match by passing a second param. This alias works only with sockets opened by * <code>/http_get</code>, and even then only after the connection has been handed over * to the calling script (IOW, when your on:sockread has run at least once). * @param sockname Socket name or wildcard * @optparam n Select Nth wildcard match * @prop rcvd Number of bytes received, not counting HTTP-headers * @prop redirects Number of redirects followed (usually 0) * @prop percent Percent done, available only if .size is known * @prop size Size of the file (value of the Content-Lenght header). Server does not know * in advance the size for dynamic content so this works only with static files. * @return int Numeric value, $null if the value isn't known (size/percent) */ alias httpsock { var %sockname = $1, %n = $2 if (%n == $null) %n = 1 if ($sock(%sockname,%n) == $null) return if ($prop == rcvd) return $calc($sock(%sockname,%n).rcvd - $gettok($sock(%sockname,%n).mark,1,32)) if ($prop == size) return $gettok($sock(%sockname,%n).mark,3,32) if ($prop == redirects) return $gettok($sock(%sockname,%n).mark,2,32) if ($prop == percent) { if ($gettok($sock(%sockname,%n).mark,3,32) == $null) return $null return $int($calc(($sock(%sockname,%n).rcvd - $gettok($sock(%sockname,%n).mark,1,32)) / $gettok($sock(%sockname,%n).mark,3,32) * 100)) } } ;;;;;;;;;;;; Public API ends here. Rest is the internal workings of LibHTTP ;;;;;;;;;;;; /**************************************************************************************** * Parses URL and returns a space separated list of URL-components, suitable for input * to <code>/http_get</code>. * @param URL The url to parse * @return String Host Port Path [User] [Pass] */ alias -l lhttp_parse_url { var %url = $1 var %port = 80 var %host = $gettok($remove(%url,http://),1,47) var %path = / $+ $gettok($remove(%url,http://),2-,47) if ($regex(%host,/^(.+):(.+)@(.+)$/)) { var %user = $regml(1), %pass = $regml(2), %host = $regml(3) } if ($regex(%host,/^(.+):(\d+)$/)) { var %host = $regml(1), %port = $regml(2) } return %host %port %path %user %pass } /**************************************************************************************** * This is what actually opens the connection. http_get is really just a wrapper, this * where the magic happens. The point of having a wrapper is to be able to change this * API without breaking existing programs. Programs use the public, very simple API of * the wrapper that never changes, internally we have this more complex and freely * changable thing. * <p> * The socket is opened with a temporary name, LIBTTP_TEMP_[number]. A hash table of the * same name is also created, it is used for storing both request and response headers * plus other misc data LibHTTP uses. The hashtable is automatically hfreed if an error * occurs, or when control is handed over to user (socket is renamed). * * @param finalname The final name for this connections * @param redirects Number of redirects followed so far * @param host Hostname or IP-address of the server * @param port Port to connect to (usually 80) * @param path Path of the requested resource * @optparam user Username (if the server requires one) * @optparam pass Password (if the server requires one) * @return void */ alias -l lhttp_sockopen { var %finalname = $1, %redirects = $2, %host = $3, %port = $4, %path = $5, %user = $6, %pass = $7, %auth = none if (%redirects > 5) { .signal HttpErr_ $+ %finalname Redirect limit (5) exceeded - likely an infinite redirect loop. return } var %i = 1 while ($sock(LIBHTTP_TEMP_ $+ %i) != $null) inc %i var %tempname = LIBHTTP_TEMP_ $+ %i hmake %tempname if ((%libhttp.useproxy) && (%libhttp.proxy.host != $null) && (%libhttp.proxy.port != $null)) { if (%libhttp.debug) echo -s *** sockopen %tempname %libhttp.proxy.host %libhttp.proxy.port sockopen %tempname %libhttp.proxy.host %libhttp.proxy.port hadd %tempname _Request-String GET http:// $+ %host $+ : $+ %port $+ %path HTTP/1.0 hadd %tempname _Connection-Host %libhttp.proxy.host hadd %tempname _Connection-Port %libhttp.proxy.port if ((%libhttp.proxy.user != $null) && (%libhttp.proxy.pass != $null)) { hadd %tempname Proxy-Authorization: Basic $encode(%libhttp.proxy.user $+ : $+ %libhttp.proxy.pass,m) } hadd %tempname Proxy-Connection: close } else { if (%libhttp.debug) echo -s *** sockopen %tempname %host %port sockopen %tempname %host %port hadd %tempname _Request-String GET %path HTTP/1.0 hadd %tempname _Connection-Host %host hadd %tempname _Connection-Port %port } ; Vars with leading underscore are for internal use of LibHTTP hadd %tempname _Redirects-Followed %redirects hadd %tempname _Final-Sockname %finalname ; Vars without leading underscore are headers hadd %tempname Host: %host $+ : $+ %port hadd %tempname Pragma: no-cache hadd %tempname Cache-Control: no-cache hadd %tempname Connection: close hadd %tempname User-Agent: Mirc/ $+ $version (Windows $os $+ ) LibHTTP/1.0 if ((%user != $null) && (%pass != $null)) { hadd %tempname Authorization: Basic $encode(%user $+ : $+ %pass,m) } } /**************************************************************************************** * Writes the HTTP-request to socket. The request string and the headers were constructed * in lhttp_sockopen, this just loops throught the headers and sends them over the socket. * <p> * It is very important that this event be "duplication tolerant", meaning it must still * work if user has two copies of this script installed (happens if two independently * distributed scripts bundle LibHTTP). First script to handle this sockopen event sets a * hash table item "_Request-Sent" to $true - second script detects the presence of that * flag and returns immediately. "if ($hget($sockname) != $null)" in the sockerr-branch * of the code provides the same safeguard in error conditions. */ on *:sockopen:LIBHTTP_TEMP_*: { if ($sockerr > 0) { if ($hget($sockname) != $null) { .signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Unable to connect to $hget($sockname,_Connection-Host) $+ , port $hget($sockname,_Connection-Port) hfree $sockname } return } if ($hget($sockname,_Request-Sent)) return hadd $sockname _Request-Sent $true if (%libhttp.debug) echo -s --> $hget($sockname,_Request-String) sockwrite -n $sockname $hget($sockname,_Request-String) var %hindex = $hget($sockname,0).item while (%hindex > 0) { var %key = $hget($sockname,%hindex).item if (_* iswm %key) { ; Key is some internal var, not a header } else { sockwrite -n $sockname %key $hget($sockname,%key) if (%libhttp.debug) echo -s --> %key $hget($sockname,%key) } dec %hindex } sockwrite $sockname $crlf .timer $+ $sockname $+ _TIMEOUT 1 30 lhttp_timeout $sockname } /**************************************************************************************** * Read and save headers. Once headers have been fully consumed, calls http_process_reply * which determines what to do next. * <p> * Duplication tolerance is needed here also: Ordinarily getting $null from sockread * indicates end of headers - with two scripts installed that may not always be correct. * Since the sockread event now triggers twice whenever there is a CRLF terminated line * in the buffer, it is possible that the first script consumes a normal headerline, * leaving the buffer completely empty. When the second script triggers, sockread * tries to read from empty buffer and returns $null. Thus it is crucial to test $sockbr * to verify that we indeed got an "empty" line, instead of just hitting empty buffer. */ on *:sockread:LIBHTTP_TEMP_*: { if ($sockerr > 0) { if ($hget($sockname) != $null) { .signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Connection lost while reading headers (sockerr $sockerr $+ ) .timer $+ $sockname $+ _TIMEOUT off hfree $sockname } return } var %sockinput sockread %sockinput if (%libhttp.debug) echo -s <-- %sockinput [ $+ $sockbr bytes] if (%sockinput != $null) { var %header = $gettok(%sockinput,1,32), %value = $gettok(%sockinput,2-,32) if (HTTP/1.? iswm %header) { hadd $sockname _Response-Code $gettok(%value,1,32) } else { hadd $sockname _Response-Header_ $+ %header %value } .timer $+ $sockname $+ _TIMEOUT 1 30 lhttp_timeout $sockname } elseif ($sockbr > 0) { lhttp_process_reply $sockname } } /**************************************************************************************** * Headers have been read, determine the next step. If server replied with code 200, * rename the socket and hand over control. If the server replied with code 301 or 302 AND * sent a Location: header, follow the redirect and close current connection. If the server * replied with any other code, signal an error. @return void */ alias -l lhttp_process_reply { var %sockname = $1 var %respcode = $hget(%sockname,_Response-Code) var %location = $hget(%sockname,_Response-Header_Location:) var %filesize = $hget(%sockname,_Response-Header_Content-Length:) var %finalname = $hget(%sockname,_Final-Sockname) var %redirects = $hget(%sockname,_Redirects-Followed) if (%respcode == 200) { sockmark %sockname $sock(%sockname).rcvd %redirects %filesize if (%libhttp.debug) echo -s *** sockrename %sockname %finalname sockrename %sockname %finalname } elseif ((%respcode isnum 301-302) && (%location != $null)) { lhttp_sockopen %finalname $calc(%redirects + 1) $lhttp_parse_url(%location) if (%libhttp.debug) echo -s *** sockclose %sockname sockclose %sockname } else { .signal HttpErr_ $+ %finalname Server returned error %respcode if (%libhttp.debug) echo -s *** sockclose %sockname sockclose $sockname } hfree %sockname .timer $+ %sockname $+ _TIMEOUT off } /**************************************************************************************** * 30 second timeout for the temp-socket. If server doesn't respond within 30 seconds of * sending the request, or if the connection goes idle for more than 30 sec while reading * headers, close the socket and signal an error. @return void */ alias -l lhttp_timeout { var %sockname = $1 if ($hget(%sockname) != $null) { .signal HttpErr_ $+ $hget(%sockname,_Final-Sockname) Connection timeout of 30 seconds reached while awaiting server responce sockclose %sockname hfree %sockname } } /**************************************************************************************** * Connection closed while we were still reading headers */ on *:sockclose:LIBHTTP_TEMP_*: { if ($hget($sockname) != $null) { .signal HttpErr_ $+ $hget($sockname,_Final-Sockname) Connection lost while reading headers (sockerr: $sockerr $+ ) hfree $sockname } } /**************************************************************************************** * Fancy dialog for proxy configuration */ menu status { Config HTTP Proxy: dialog -md ProxyConfig ProxyConfig_Table } dialog ProxyConfig_Table { size -1 -1 250 150 title "HTTP Proxy Configuration" check "Use Proxy for HTTP connections", 1, 5 5 180 14 box "Server and Port" 10, 6 22 240 75 edit "" 11, 12 38 165 24, autohs disabled text ":" 12, 179 41 3 14 edit "" 13, 183 38 55 24, autohs disabled text "Status:" 14, 20 69 40 14 text "" 15, 60 69 170 14 box "Login and Password (optional)" 20, 6 89 240 55 edit "" 21, 12 110 110 24, autohs disabled edit "" 22, 128 110 110 24, autohs pass disabled } /**************************************************************************************** * Proxy configuration dialog init */ on *:dialog:ProxyConfig:init:0: { did -ra $dname 11 %libhttp.proxy.host did -ra $dname 13 %libhttp.proxy.port did -ra $dname 21 %libhttp.proxy.user did -ra $dname 22 %libhttp.proxy.pass if (%libhttp.useproxy) { did -c $dname 1 | did -e $dname 11,13,21,22 } lhttp_proxytest } /**************************************************************************************** * User toggled the "Use Proxy" checkbox */ on *:dialog:ProxyConfig:sclick:1: { if ($did($dname,$did).state == 1) { %libhttp.useproxy = $true | did -e $dname 11,13,21,22 } if ($did($dname,$did).state == 0) { %libhttp.useproxy = $false | did -b $dname 11,13,21,22 } lhttp_proxytest } /**************************************************************************************** * User edited one of the four text inputs */ on *:dialog:ProxyConfig:edit:*: { %libhttp.proxy.host = $did($dname,11) %libhttp.proxy.port = $did($dname,13) %libhttp.proxy.user = $did($dname,21) %libhttp.proxy.pass = $did($dname,22) if ($did < 20) .timerTEST_PROXY 1 1 lhttp_proxytest } /**************************************************************************************** * Test proxy address by attempting to connect to it @param void @return void */ alias -l lhttp_proxytest { if ($dialog(ProxyConfig) == $null) return if ($sock(LIBHTTP_PROXYTEST) != $null) sockclose LIBHTTP_PROXYTEST if ((%libhttp.proxy.host != $null) && (%libhttp.proxy.port != $null) && (%libhttp.useproxy)) { sockopen LIBHTTP_PROXYTEST %libhttp.proxy.host %libhttp.proxy.port did -ra ProxyConfig 15 Testing.... } else did -r ProxyConfig 15 } /**************************************************************************************** * Show result of the proxy test */ on *:sockopen:LIBHTTP_PROXYTEST: { if ($dialog(ProxyConfig) != $null) did -ra ProxyConfig 15 $iif($sockerr, Unable to connect, OK) sockclose $sockname } /**************************************************************************************** * Unset global variables used by libhttp */ on *:UNLOAD: { unset %libhttp.* }